﻿using System;

using System.Collections.Generic;
using Silvermoon.OpenGL.Native;
using System.Drawing;
using Silvermoon.OpenGL;
using Silvermoon.Controls;
using Silvermoon.Visuals;
using Silvermoon.Utils;
using Silvermoon.Controls.Interfaces;
using Silvermoon.Controls.Classes;
using Silvermoon.Enums;

namespace Silvermoon.Core
{
    /// <summary>
    /// Renders Controls, Visuals and Elements using OpenGL.
    /// </summary>
    public class Renderer
    {
        #region structs

        private struct TransformData
        {
            public int Alpha;
            public float X0;
            public float Y0;

            public int X { get { return (int)X0; } }
            public int Y { get { return (int)Y0; } }
        }


        #endregion
        #region fields

        const int one = 65536;
        const int maxStackDepth = 16;
        public readonly Color StartTextColor = Color.White;

        private bool scale;
        private Rectangle currentScissor;
        private Color[] textColorStack = new Color[maxStackDepth];
        int textColorDepth = 0;
        private Rectangle[] scissorStack = new Rectangle[maxStackDepth];
        private int scissorDepth;

        private Rectangle scissorBox;
        private Color backgroundColor = Color.Black;
        protected Device device { get; private set; }
        protected Capabilities capabilities { get; private set; }


        #endregion
        #region ctors/dtors

        internal Renderer(Device device, Rectangle clientRectangle)
        {
            if (device == null) throw new ArgumentNullException("device");

            Perspective = true;
            this.ClientRectangle = clientRectangle;
            Invalidated = true;
            this.device = device;
            this.capabilities = device.Capabilities;
            Init();
        }

        #endregion
        #region props

        /// <summary>
        /// Gets the color that is the current color to render text or drawings.
        /// </summary>
        public Color TextColor { get; private set; }

        /// <summary>
        /// Gets the current alpha value.
        /// </summary>
        public int Alpha { get; private set; }

        /// <summary>
        /// Specifies how controls are actually scaled.
        /// Note that this does not reflect visuals since they are already scaled.
        /// </summary>
        protected SizeF ScaleFactor { get; private set; }

        public Device Device { get { return device; } }
        public bool Perspective { get; set; }
        public bool EnableScissor = true;
        public bool Invalidated { get; set; }

        public Rectangle ClientRectangle { get; private set; }

        /// <summary>
        /// Gets whether the renderer is currently rendering.
        /// </summary>
        public bool IsRendering { get; private set; }

        protected Rectangle ScreenBounds { get; private set; }

        public Color BackgroundColor
        {
            get { return backgroundColor; }
            set
            {
                backgroundColor = value;
                device.ClearColor(value);
            }
        }

        protected Rectangle ScissorBox
        {
            get { return scissorBox; }
            set
            {
                if (scissorBox != value)
                {
                    scissorBox = value;
                    int cx = value.X;
                    int cy = ScreenBounds.Height - (value.Bottom);
                    gl.Scissor(cx, cy, value.Width, value.Height);
                }
            }
        }

        #endregion
        #region methods

        private void Init()
        {
            int v = gl.GetIntegerValue(ParameterName.StencilBits);

            device.ClearColor(BackgroundColor);
            device.SetClearDepthf(9.0f);
            capabilities.DepthTest = false;
            capabilities.AlphaTest = false;
            capabilities.Normalize = false;
            device.ClientState.ColorArray = false;
            capabilities.Blend = true;
            device.SetAlphaFunction(Comparison.Always, BlendFactor.SrcAlpha);
            //device.SetAlphaFunction(Comparison.Always, BlendFactor.OneMinusSrcAlpha);
            device.SetBlendFunction(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha);
            device.DepthFunction = Comparison.Never;
            device.SetHint(HintTarget.PerspectiveCorrection, HintMode.Fastest);
            capabilities.PointSmooth = false;
            capabilities.LineSmooth = true;
            capabilities.Dither = true;
            capabilities.CullFace = false;
            device.ShadeModel = ShadeModel.Smoth;

            capabilities.ScissorTest = EnableScissor;

        }


        public void Render(IEnumerable<Control> controls)
        {
            IsRendering = true;
            try
            {
                Rectangle bounds = InitializeRender();
                BeginRender();

                if (controls != null) RenderControls(controls, 0, 0, 255);

                device.Present();
                Invalidated = false;
            }
            finally
            {
                IsRendering = false;
            }
        }

        private Rectangle InitializeRender()
        {
            scale = Screen.ControlScaleFactor.Width != 1f;
            ScaleFactor = Screen.ControlScaleFactor;

            textColorDepth = 0;
            TextColor = StartTextColor;
            Rectangle bounds = ClientRectangle;
            this.ScreenBounds = bounds;
            ResetScissorStack(bounds);
            AdjustMatrix();
            return bounds;
        }

        private void RenderControls(IEnumerable<Control> controls, int x, int y, int alpha)
        {

            foreach (Control c in controls)
            {
                if (c.Visible && c.Alpha >= 4)
                {
                    RenderControl(c, x, y, alpha);
                }
            }
        }

        /// <summary>
        /// Occurs before rendering all controls.
        /// </summary>
        private void BeginRender()
        {
            Rectangle bounds = ScreenBounds;

            Capabilities caps = capabilities;
            caps.Normalize = false;
            caps.DepthTest = false;
            capabilities.ScissorTest = EnableScissor;
            if (EnableScissor)
            {
                ScissorBox = new Rectangle(0, 0, bounds.Width, bounds.Height);
            }

            device.SetMatrixMode(MatrixMode.ModelView);
            device.LoadIdentity();

            if (!this.BackgroundColor.IsEmpty) gl.Clear(ColorBufferBit.Color);
            device.SetColorMask(true, true, true, true);
            gl.Color4f(1f, 1f, 1f, 1f);
            gl.ShadeModel(ShadeModel.Smoth);
            gl.EnableClientState(ClientStateArray.Vertex);
            gl.ActiveTexture(TextureUnit.Texture0);
            gl.ClientActiveTexture(TextureUnit.Texture0);
            gl.EnableClientState(ClientStateArray.TextureCoord);
            gl.Enable(Capability.Texture2D);
        }

        private void AdjustMatrix()
        {
            Rectangle bounds = ClientRectangle;
            device.SetMatrixMode(MatrixMode.Projection);
            device.LoadIdentity();
            if (Perspective)
            {
                gl.Frustumf(-bounds.Width / 20f, bounds.Width / 20f, bounds.Height / 20f, -bounds.Height / 20f, 1F, 100f);
                gl.Translatef(-(float)bounds.Width / 2f, -(float)bounds.Height / 2f, -10f);
                gl.Scalef(1f, 1f, 0.007f);
            }
            else
            {
                device.Orthof(0, bounds.Width, bounds.Height, 0F, -100F, 100F);
            }
        }

        /// <summary>
        /// Renders a controls
        /// </summary>
        /// <param name="control">The control to render.</param>        
        private void RenderControl(Control control, int x, int y, int alpha)
        {
            textColorStack[textColorDepth++] = TextColor;
            if (control.Alpha < 255) alpha = (control.Alpha * alpha) >> 8;
            if (!control.TextColor.IsEmpty)
            {
                TextColor = ColorUtil.AlphaColor(control.TextColor, (control.TextColor.A * alpha) >> 8);
            }
            else if (alpha < 255 || control.Alpha < 255)
            {
                TextColor = ColorUtil.AlphaColor(TextColor, (TextColor.A * alpha >> 8));
            }
            Rectangle bounds = control.Bounds;
            gl.PushMatrix();
            if (control.Invalidated) control.PreLoad();

            if (EnableScissor)
            {
                Rectangle r = control.ActualBounds;
                r.Offset(x, y);
                if (scale) r = Control.Scale(r);
                PushScissor(r);
            }
            if ((control.Flags & ControlFlags.IgnoreClipping) != 0) gl.Disable(Capability.ScissorTest);


            if (scale)
            {
                gl.Translatef(control.X * ScaleFactor.Width, control.Y * ScaleFactor.Height, 0f);
            }
            else
            {
                gl.Translatef((control.X), (control.Y), 0f);
            }

            if (control.IsAnimated || control.HasTransformations)
            {
                TransformData transformData = new TransformData { Alpha = alpha };

                // note that control.Bound.Size is not necassarily control.Size:
                if (control.IsAnimated)
                {
                    IControlAnimation a = (IControlAnimation)control;
                    ApplyTransformations(a.AnimationTransformations, control.Bounds.Size, ref transformData);
                }
                if (control.HasTransformations)
                {
                    ApplyTransformations(control.Transformations, control.Bounds.Size, ref transformData);
                }
                x += transformData.X;
                y += transformData.Y;
                alpha = transformData.Alpha;
            }

            this.Alpha = alpha;

            if (control.Visuals != null)
            {
                foreach (Visual v in control.Visuals)
                {
                    if (v.Visible) RenderVisual(v, alpha);
                }
            }

            if (control.SubControls != null)
            {
                bool enableScissor = EnableScissor;
                bool scissor = (control.Flags & ControlFlags.IgnoreChildClipping) == 0;
                if (!scissor) EnableScissor = false;
                if (enableScissor && !control.Clipping.IsEmpty)
                {
                    // if there is a Margin specified, clip to that margin when rendering the child controls:
                    Rectangle r = control.ClientBounds;
                    r.Offset(control.X, control.Y);
                    if (scale) r = Control.Scale(r);
                    PushScissor(r);
                    RenderControls(control.SubControls, x + control.X, y + control.Y, alpha);
                    PopScissor();
                }
                else
                {
                    RenderControls(control.SubControls, x + control.X, y + control.Y, alpha);
                }
                EnableScissor = enableScissor;
            }

            if (EnableScissor) PopScissor();

            gl.PopMatrix();
            TextColor = textColorStack[--textColorDepth];
            gl.Enable(Capability.ScissorTest);
        }

        private Point GetTransformationPoint(IList<Transformation> transformations)
        {
            float x0 = 0F;
            float y0 = 0F;
            foreach (Transformation t in transformations)
            {
                TranslateTransform tt = t as TranslateTransform;
                if (tt != null)
                {
                    x0 += tt.OffsetX;
                    y0 += tt.OffsetY;
                }
            }
            return new Point((int)x0, (int)y0);
        }

        /// <summary>
        /// Applies all specified transformations.
        /// </summary>
        /// <param name="transformations">An enumeration of transformations</param>
        /// <param name="size">The actual size of the control.</param>
        /// <param name="alpha">The current alpha value for rendering.</param>
        /// <returns>The new alpha value.</returns>
        private void ApplyTransformations(IEnumerable<Transformation> transformations, Size size, ref TransformData data)
        {
            if (transformations != null)
            {
                foreach (Transformation t in transformations)
                {
                    switch (t.Id)
                    {
                        case TransformationId.Rotate: ApplyRotateTransform(size, (RotateTransform)t); break;
                        case TransformationId.Transparent: data.Alpha = ((TransparentTransform)t).Alpha * data.Alpha / 255; break;
                        case TransformationId.Scale: ApplyScaleTransform(size, (ScaleTransform)t); break;
                        case TransformationId.Translate: ApplyTranslateTransform(size, (TranslateTransform)t, ref data); break;
                        case TransformationId.Zoom: ApplyStretchTransform(size, (StretchTransform)t); break;
                    }
                }
            }
        }

        private void ApplyTranslateTransform(Size size, TranslateTransform transform, ref TransformData data)
        {
            gl.Translatef(transform.OffsetX, transform.OffsetY, transform.OffsetZ);
            data.X0 += transform.OffsetX;
            data.Y0 += transform.OffsetY;
        }

        /// <summary>
        /// Scales an element.
        /// </summary>
        /// <remarks>
        /// The view matrix positon of the control is currently set to 0/0 thus, to rotate the control arround it's center,
        /// the view matrix must first be translated by the half width and height of the control.
        /// </remarks>
        /// <param name="control">The control to rotate.</param>
        /// <param name="rotateTransform">The transformation info.</param>
        private void ApplyScaleTransform(Size size, ScaleTransform transform)
        {
            int x = size.Width / 2;
            int y = size.Height / 2;
            gl.Translatef(x, y, 0);

            gl.Scalef(transform.X, transform.Y, transform.Z);
            gl.Translatef(-x, -y, 0);
        }

        /// <summary>
        /// Scales an element.
        /// </summary>
        /// <remarks>
        /// The view matrix positon of the control is currently set to 0/0 thus, to rotate the control arround it's center,
        /// the view matrix must first be translated by the half width and height of the control.
        /// </remarks>
        /// <param name="control">The control to rotate.</param>
        /// <param name="rotateTransform">The transformation info.</param>
        private void ApplyStretchTransform(Size size, StretchTransform transform)
        {
            gl.Scalef(transform.X, transform.Y, transform.Z);
        }

        /// <summary>
        /// Rotates an element.
        /// </summary>
        /// <remarks>
        /// The view matrix positon of the control is currently set to 0/0 thus, to rotate the control arround it's center,
        /// the view matrix must first be translated by the half width and height of the control.
        /// </remarks>
        /// <param name="control">The control to rotate.</param>
        /// <param name="rotateTransform">The transformation info.</param>
        private void ApplyRotateTransform(Size size, RotateTransform rotateTransform)
        {
            int x = size.Width / 2;
            int y = size.Height / 2;

            //if (rotateTransform.Axis != RotateAxis.ZAxis) PespectiveRequired = true;
            gl.Translatef(x, y, 0);

            RotateAxis axis = rotateTransform.Axis;
            float ax = axis == RotateAxis.XAxis ? 1F : 0F;
            float ay = axis == RotateAxis.YAxis ? 1F : 0F;
            float az = axis == RotateAxis.ZAxis ? 1F : 0F;
            gl.Rotatef(rotateTransform.Angle, ax, ay, az);
            gl.Translatef(-x, -y, 0);
        }

        /// <summary>
        /// Renders a visual.
        /// </summary>
        /// <param name="visual">The visual to render.</param>
        /// <param name="alpha">The alpha value for the view that specifies if the visual is transparent or opaque. Acceptable values are between 0 and 255.</param>
        private void RenderVisual(Visual visual, int alpha)
        {
            int x = visual.X;
            int y = visual.Y;

            gl.PushMatrix();
            gl.Translatef((float)x, (float)y, 0f);
            if (visual.Transformations != null)
            {
                TransformData data = new TransformData { Alpha = alpha };
                ApplyTransformations(visual.Transformations, visual.Bounds.Size, ref data);
                alpha = data.Alpha;
            }
            PrepareVisual();
            Color c = visual.PrimitiveColor;
            if (visual.Alpha < 255)
            {
                alpha = (alpha * visual.Alpha) >> 8;
            }

            if (c != Color.Empty)
            {
                SetColor(c, alpha);
            }
            else
            {
                gl.Color4x(one, one, one, alpha << 8);
            }

            visual.Render(this);
            gl.PopMatrix();
        }

        /// <summary>
        /// Occurs before rendering a Visual.
        /// </summary>
        /// 
        /// 
        private void PrepareVisual()
        {
            gl.DisableClientState(ClientStateArray.Color);
            //gl.DisableClientState(ClientStateArray.TextureCoord);
            //gl.Disable(Capability.Texture2D);

        }

        private void SetColor(Color c, int alpha)
        {
            int r = c.R;
            int g = c.G;
            int b = c.B;
            int a = (c.A * alpha) / 255;
            gl.Color4x(r << 8, g << 8, b << 8, a << 8);
        }

        private void PushScissor(Rectangle bounds)
        {
            scissorStack[scissorDepth++] = currentScissor;
            Rectangle r = RectangleUtil.Cut(bounds, currentScissor);
            currentScissor = r;
            ScissorBox = r;
        }

        private void SetScissor(Rectangle bounds)
        {
            scissorStack[scissorDepth++] = currentScissor;
            Rectangle r = bounds;
            currentScissor = r;
            ScissorBox = r;
        }

        private void PopScissor()
        {
            currentScissor = scissorStack[--scissorDepth];
        }

        private void ResetScissorStack(Rectangle bounds)
        {
            scissorDepth = 0;
            currentScissor = bounds;
        }

        public void ChangeSize(Size size)
        {
            this.ClientRectangle = new Rectangle(0, 0, size.Width, size.Height);
            Init();
            Invalidated = true;
        }

        private Texture Capture(Control c, int x, int y)
        {
            Texture texture = new Texture();
            Rectangle clip = new Rectangle(x, y, c.Width, c.Height);
            clip.Y = ClientRectangle.Height - clip.Bottom;
            try
            {
                texture.Capture(clip);
            }
            catch (OpenGlException)
            {
                texture.Dispose();
                texture = null;
            }
            return texture;
        }

        /// <summary>
        /// Captures the specified control and returns a texture that contains the complete image of the control.
        /// Note that Capture ignores Transformations and Alpha value.
        /// </summary>
        /// <param name="c">The control to capture.</param>
        /// <returns>A texture that contains the image of the specified control, otherwise null.</returns>
        internal Texture Capture(Control c)
        {
            InitializeRender();
            BeginRender();
            Point location = c.Location;
            TextColor = Screen.Current.GetTextColor(c);
            int alpha = c.Alpha;
            IEnumerable<Transformation> transformations = c.Transformations;
            c.Location = Point.Empty;
            c.Transformations = null;
            c.Alpha = 255;
            try
            {
                RenderControl(c, 0, 0, 255);
            }
            finally
            {
                c.Alpha = alpha;
                c.Transformations = transformations;
                c.Location = location;
            }
            Invalidated = true;
            return Capture(c, 0, 0);
        }

        #endregion

    }
}
